#include "widgetpaincomplex.h"
#include "ui_widgetpaincomplex.h"
#include <QDebug>
#include <QMessageBox>

WidgetPAInComplex::WidgetPAInComplex(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::WidgetPAInComplex)
{
    ui->setupUi(this);
    //指针初始化为空
    m_streamIn = NULL;
    //先初始化 PA 库
    Pa_Initialize();

    //调用初始化界面函数，音频框架和设备的组合框需要用 PA 库的函数
    initUI();
}

WidgetPAInComplex::~WidgetPAInComplex()
{
    //关闭文件和套接字
    m_file.close();
    m_socket.close();
    //指针置空
    m_streamIn = NULL;
    //终止 PA 库引用
    Pa_Terminate();
    //销毁窗口
    delete ui;
}

//初始化界面
void WidgetPAInComplex::initUI()
{
    //获取音频框架
    int nAPICount = Pa_GetHostApiCount();
    QString strTemp; //用于构造音频框架字符串
    for(int i=0; i<nAPICount; i++)//逐个添加框架名称到组合框
    {
        const PaHostApiInfo *pAPIInfo = Pa_GetHostApiInfo( i );
        strTemp = QString::fromLocal8Bit( pAPIInfo->name );
        ui->comboBoxHostAPI->addItem( strTemp );
    }
    //添加头一个音频框架时触发该组合框序号变化信号
    //该信号的槽函数自动添加音频框架管理的设备
    //声道数量
    QStringList listChannels;
    listChannels<<"1"<<"2";
    ui->comboBoxChannels->addItems( listChannels );
    //采样位宽，默认用 uint8，int16，int24，float32
    QStringList listSampleSizes;
    listSampleSizes<<"8"<<"16"<<"24"<<"32";
    ui->comboBoxSampleSize->addItems( listSampleSizes );
    //默认用16bit采样
    ui->comboBoxSampleSize->setCurrentText( "16" );
    //采样频率
    QStringList listSampleRates;
    listSampleRates<<"8000"<<"22050"<<"44100"<<"48000"<<"96000";
    ui->comboBoxSampleRate->addItems( listSampleRates );
    //时间片帧数范围，8k采样一毫秒8帧，96k采样一秒96000帧
    ui->spinBoxFramesPerBuffer->setRange(8, 96000);
    //默认 8k 采样 20 毫秒，就是 160 帧
    ui->spinBoxFramesPerBuffer->setValue( 160 );

    //默认IP地址
    ui->lineEditIP->setText( "127.0.0.1" );
    //端口号范围
    ui->spinBoxPort->setRange( 1, 65535 );
    ui->spinBoxPort->setValue( 12345 ); //默认端口
    //默认文件名
    ui->lineEditFile->setText( "E:/pacomplex_8k1c16bit.pcm" );
    //界面设置完毕
    return;
}

//自动根据音频框架的变化，更新框架管理的设备
void WidgetPAInComplex::on_comboBoxHostAPI_currentIndexChanged(int index)
{
    if(index < 0)   return; //音频框架序号不合法
    //清空旧设备
    ui->comboBoxDevices->clear();
    //获取当前音频框架
    const PaHostApiInfo *pAPIInfo = Pa_GetHostApiInfo( index );
    int nAPIDeviceCount = pAPIInfo->deviceCount; //框架管理的设备计数
    QString strTemp;
    //添加该音频框架管理的设备到组合框
    for(int i=0; i<nAPIDeviceCount; i++)
    {
        //根据设备相对序号获取绝对序号，index是框架序号，i 是框架设备相对序号
        int nGlobali = Pa_HostApiDeviceIndexToDeviceIndex(index, i);
        //获取设备信息
        const PaDeviceInfo *pDevInfo = Pa_GetDeviceInfo( nGlobali );
        //Windows设备名称字符串是本土的 GBK 编码，需要转换一下
        strTemp = QString::fromLocal8Bit( pDevInfo->name );
        ui->comboBoxDevices->addItem( strTemp );
    }
}

//先编写回调函数，用户数据保存窗口类对象的this指针
static int audioInCallback(const void *inputBuffer,
                       void *outputBuffer,
                       unsigned long frameCount,
                       const PaStreamCallbackTimeInfo *timeInfo,
                       PaStreamCallbackFlags statusFlags,
                       void *userData)
{
    //采集的数据，保存文件和发送网络只论字节数，不管数据格式
    const char* dataIn = (const char*)inputBuffer;
    //窗口指针
    WidgetPAInComplex *pWnd = (WidgetPAInComplex *)userData;
    //qint64 nsBegin = pWnd->m_calcTime.nsecsElapsed();
    //计算数据字节数
    int nChannels = pWnd->m_nChannels;//通道数
    int nBytesPerSample = (pWnd->m_nSampleSize) / 8; //样本字节数
    int nAllBytes = frameCount*nChannels*nBytesPerSample;
    //写入文件
    (pWnd->m_file).write( dataIn, nAllBytes );
    //写入套接字
    (pWnd->m_socket).write( dataIn, nAllBytes );
    //调用状态显示函数
    pWnd->showStatus(frameCount, nAllBytes);
    //qint64 nsEnd = pWnd->m_calcTime.nsecsElapsed();
    //qDebug()<<"one call ns: "<<nsEnd - nsBegin;
    //回调函数一定要有返回值
    return paContinue; //继续采集
}

//设置状态字符串
void WidgetPAInComplex::showStatus(int nFrames, int nBytes)
{
    QString strStatus;
    qint64 msCount = m_calcTime.elapsed(); //毫秒计数
    //构造字符串
    strStatus = tr("%1.%2 s, %3 frames, %4 bytes")
            .arg( msCount / 1000 )
            .arg( msCount%1000, 3, 10, QChar('0') )
            .arg( nFrames )
            .arg( nBytes );
    ui->labelStatus->setText( strStatus );//显示到状态标签
    return;
}

//根据采样位宽获取采样点数据格式
PaSampleFormat WidgetPAInComplex::getSampleFormat(int sampleSize)
{
    switch (sampleSize) {
    case 8:
        return paUInt8;//8位无符号整数
        break;
    case 16:
        return paInt16;//16位有符号整数
        break;
    case 24:
        return paInt24;//24位有符号整数
        break;
    case 32:
        return paFloat32;//32位浮点数
        break;
    default:
        return paInt16;//默认16位有符号整数
        break;
    }
}

void WidgetPAInComplex::on_pushButtonStart_clicked()
{
    //获取音频框架序号
    int nAPIIndex = ui->comboBoxHostAPI->currentIndex();
    //设备相对序号
    int nAPIDevIndex = ui->comboBoxDevices->currentIndex();
    //转为绝对序号
    int nGlobalDevIndex = Pa_HostApiDeviceIndexToDeviceIndex(nAPIIndex, nAPIDevIndex);
    //设备信息
    const PaDeviceInfo *pDevInfo = Pa_GetDeviceInfo( nGlobalDevIndex );
    //输入声道上限
    int nMaxChannels = pDevInfo->maxInputChannels;
    //设置的输入声道数
    m_nChannels = ui->comboBoxChannels->currentText().toInt();
    QString strTemp;
    if( m_nChannels > nMaxChannels ) //超出限制
    {
        strTemp = tr("输入声道数超出设备支持上限，设置数：%1，上限数：%2")
                .arg(m_nChannels).arg(nMaxChannels) ;
        QMessageBox::warning(this, tr("声道数不支持"), strTemp);
        return;
    }
    //尝试打开文件
    m_file.setFileName( ui->lineEditFile->text() );
    if( ! m_file.open( QIODevice::WriteOnly ) )
    {
        QMessageBox::warning(this, tr("打开文件"), tr("打开文件失败，请检查文件名和权限。"));
        return;
    }
    //尝试转换字符串为IP地址对象
    QHostAddress ipAddr;
    if( ! ipAddr.setAddress( ui->lineEditIP->text() ) )
    {
        QMessageBox::warning(this, tr("IP地址"), tr("IP地址不合法。"));
        m_file.close(); //文件释放
        return;
    }
    quint16 nPort = ui->spinBoxPort->value();
    //设置 UDP 套接字的目标地址和端口
    m_socket.connectToHost(ipAddr, nPort);

    //获取界面的录音参数，通道数已判断
    m_nSampleSize = ui->comboBoxSampleSize->currentText().toInt();
    m_nSampleRate = ui->comboBoxSampleRate->currentText().toInt();
    m_nFramesPerBuffer = ui->spinBoxFramesPerBuffer->value();
    //时间片计算，浮点数
    double dblSpan = double(m_nFramesPerBuffer) / double(m_nSampleRate);

    //定义流输入参数结构体，不需要输出参数结构体
    PaStreamParameters sinParas;
    memset(&sinParas, 0, sizeof(sinParas)); //初始化为0
    //设置结构体成员
    sinParas.device = nGlobalDevIndex;//设备绝对序号 nGlobalDevIndex
    sinParas.channelCount = m_nChannels;//通道数
    sinParas.sampleFormat = getSampleFormat( m_nSampleSize );//采样点数据类型
    sinParas.suggestedLatency = dblSpan; //仅为建议值，实际值可能是时间片的几倍
    sinParas.hostApiSpecificStreamInfo = NULL;
    //判断格式是否支持
    PaError err = Pa_IsFormatSupported( &sinParas, NULL, m_nSampleRate );
    if( paFormatIsSupported != err)
    {
        strTemp = tr("Pa_IsFormatSupported error: %1").arg( Pa_GetErrorText( err ) );
        QMessageBox::warning(this, tr("Pa_IsFormatSupported"),strTemp);
        m_file.close(); //文件释放
        m_socket.disconnectFromHost();//断开
        return;
    }
    //打开流
    err = Pa_OpenStream( &m_streamIn,   //指向流句柄
                         &sinParas,     //输入流参数指针
                         NULL,          //没有输出
                         m_nSampleRate, //采样频率
                         m_nFramesPerBuffer,//缓冲区帧数
                         paNoFlag, //默认无标志
                         audioInCallback, //回调函数名称
                         (void *)this );  //窗口对象指针
    //判断
    if( paNoError != err )
    {
        strTemp = tr("Pa_OpenStream error: %1").arg( Pa_GetErrorText( err ) );
        QMessageBox::warning(this, tr("Pa_OpenStream"), strTemp);
        m_file.close(); //文件释放
        m_socket.disconnectFromHost();//断开
        return;
    }
    //开始流
    err = Pa_StartStream( m_streamIn );
    if( paNoError != err )
    {
        strTemp = tr("Pa_StartStream error: %1").arg( Pa_GetErrorText( err ) );
        QMessageBox::warning(this, tr("Pa_StartStream"),strTemp);
        m_file.close(); //文件释放
        m_socket.disconnectFromHost();//断开
        return;
    }
    m_calcTime.start(); //计时开始
    //禁用输入控件，固定参数
    holdParas(true);
}

//采集时固定参数
void WidgetPAInComplex::holdParas(bool bHold)
{
    ui->comboBoxHostAPI->setDisabled( bHold );
    ui->comboBoxDevices->setDisabled( bHold );
    ui->comboBoxChannels->setDisabled( bHold );
    ui->comboBoxSampleSize->setDisabled( bHold );
    ui->comboBoxSampleRate->setDisabled( bHold );
    ui->lineEditIP->setDisabled( bHold );
    ui->lineEditFile->setDisabled( bHold );
    ui->spinBoxFramesPerBuffer->setDisabled( bHold );
    ui->spinBoxPort->setDisabled( bHold );
    ui->pushButtonStart->setDisabled( bHold );
}

void WidgetPAInComplex::on_pushButtonStop_clicked()
{
    //判断流是否在工作
    if( Pa_IsStreamActive(m_streamIn) <= 0 )
    {
        //没有开始或出错，不用停止
        return;
    }
    PaError err;
    qint64 msCount = m_calcTime.elapsed();  //结束录音的时间
    //停止流
    err = Pa_StopStream( m_streamIn );
    err = Pa_CloseStream( m_streamIn );
    if( paNoError != err )
    {
        //显示错误消息
        QString strErr = tr("PortAudio error: %1").arg( Pa_GetErrorText( err ) );
        QMessageBox::warning(this, tr("Pa_CloseStream"), strErr);
    }
    //关闭文件
    m_file.close();
    m_socket.disconnectFromHost();//断开
    m_streamIn = NULL;
    //恢复控件
    holdParas(false);
    QString strStatus = tr("采集结束，耗时：%1.%2 s")
           .arg( msCount / 1000 )
           .arg( msCount % 1000, 3, 10, QChar('0') );
   //显示状态
   ui->labelStatus->setText( strStatus );
}
